package cn.xw.utils.security.aes;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;

/**
 * <h3>AES加密解密的五种模式封装（ECB、CBC、CFB、OFB、CTR）</h3>
 * <h4 style="color:#f66">ECB（电子密码本模式）是最简单的AES加密模式之一：</h4>
 * 它将待加密的数据块分成多个固定大小的块，并对每个块进行独立加密。由于ECB模式不引入任何随机性，因此对于相同的输入块，
 * 将始终生成相同的输出块。
 * <h4 style="color:#f66">CBC（密码分组链接模式）是使用最广泛的AES加密模式之一：</h4>
 * 与ECB不同，CBC在加密过程中引入了初始向量（IV）和前一个加密块的输出结果（作为下一个加密块的输入）。这种链接方式增加了随机性，
 * 并使得相同的输入产生不同的输出。它提供更强的安全性，适用于大多数情况。
 * <h4 style="color:#f66">CFB（密码反馈模式）：</h4>
 * CFB模式将前一个密文块作为加密函数的输入，生成一个伪随机流。它与CBC类似，但与使用解密算法的前一个密文块进行异或运算不同，
 * CFB使用加密算法的前一个输出作为密文块和明文块的异或运算。CFB 模式可以处理部分字节的加密，因此适用于流式数据。
 * <h4 style="color:#f66">OFB（输出反馈模式）：</h4>
 * OFB模式也使用一个伪随机流，类似于CFB。与CFB不同，OFB不执行异或运算，而是直接将伪随机流与明文进行异或运算。
 * OFB模式具有自同步的特性，可以在发生错误时自动恢复。
 * <h4 style="color:#f66">CTR（计数器模式）：</h4>
 * CTR模式将加密块函数应用于计数器，生成一系列密钥流块。每个密钥流块与待加密数据块进行异或操作以生成密文。
 * CTR模式可以并行化处理，并且在随机读取数据时非常高效。
 *
 * @author Anhui AntLaddie（博客园蚂蚁小哥）
 * @version 1.0
 **/
public class AESTools {

    private static final Logger log = LoggerFactory.getLogger(AESTools.class);

    // 加密解密的十六进制密钥，可通过generateAesKey()方法生成（密钥不可暴露）
    // 若密钥固定可以写死，然后通过 .SECRET_KEY方式获取
    public static final String SECRET_KEY = "8FF674BCF9E78A53F06C68780DD6707C";

    // 初始向量信息；长度不可超过或小于16位，不可为汉字
    public static final String STR_IV = "@#RFE$%#458rh%RY";

    // 设置算法类型
    private static final String ALGORITHM = "AES";
    // 字符集编码方式
    private static final String CHARSET = "UTF-8";

    // 指定AES加密的五种模式：算法类型/模式/填充方式
    private static final String ECB_MODE = "AES/ECB/PKCS5Padding";      // 电子密码本模式
    private static final String CBC_MODE = "AES/CBC/PKCS5Padding";      // 密码分组链接模式
    private static final String CFB_MODE = "AES/CFB/PKCS5Padding";      // 密码反馈模式
    private static final String OFB_MODE = "AES/OFB/PKCS5Padding";      // 输出反馈模式
    private static final String CTR_MODE = "AES/CTR/NoPadding";         // 计数器模式

    // 链式反馈模式和计数器模式  特殊模式
    private static final List<String> specialModes = Arrays.asList(CBC_MODE, CFB_MODE, OFB_MODE, CTR_MODE);

    /**
     * 使用AES算法的ECB模式进行加密
     *
     * @param plaintext 明文数据
     * @param hexKey    十六进制的密钥
     * @return 加密后的Base64编码字符串
     */
    public static String encryptECB(String plaintext, String hexKey) {
        return encryptOrDecrypt(plaintext, hexKey, null, ECB_MODE, Cipher.ENCRYPT_MODE);
    }

    /**
     * 使用AES算法的ECB模式进行解密
     *
     * @param ciphertext 加密后的Base64编码字符串
     * @param hexKey     十六进制的密钥
     * @return 解密后的明文数据
     */
    public static String decryptECB(String ciphertext, String hexKey) {
        return encryptOrDecrypt(ciphertext, hexKey, null, ECB_MODE, Cipher.DECRYPT_MODE);
    }

    /**
     * 使用AES算法的CBC模式进行加密
     * <p style="color:#ff0">说明：strIV长度不可超过16字节，不要使用汉字</p>
     *
     * @param plaintext 明文数据
     * @param hexKey    十六进制的密钥
     * @param strIV     初始化向量（IV）。注：初始向量必须16位字符
     * @return 加密后的 Base64 编码字符串
     */
    public static String encryptCBC(String plaintext, String hexKey, String strIV) {
        return encryptOrDecrypt(plaintext, hexKey, strIV, CBC_MODE, Cipher.ENCRYPT_MODE);
    }

    /**
     * 使用AES算法的CBC模式进行解密
     * <p style="color:#ff0">说明：strIV长度不可超过16字节，不要使用汉字</p>
     *
     * @param ciphertext 加密后的 Base64 编码字符串
     * @param hexKey     十六进制的密钥
     * @param strIV      初始化向量（IV）。注：初始向量必须16位字符
     * @return 解密后的明文数据
     */
    public static String decryptCBC(String ciphertext, String hexKey, String strIV) {
        return encryptOrDecrypt(ciphertext, hexKey, strIV, CBC_MODE, Cipher.DECRYPT_MODE);
    }

    /**
     * 使用AES算法的CFB模式进行加密
     * <p style="color:#ff0">说明：strIV长度不可超过16字节，不要使用汉字</p>
     *
     * @param plaintext 明文数据
     * @param hexKey    十六进制的密钥
     * @param strIV     初始化向量（IV）。注：初始向量必须16位字符
     * @return 加密后的 Base64 编码字符串
     */
    public static String encryptCFB(String plaintext, String hexKey, String strIV) {
        return encryptOrDecrypt(plaintext, hexKey, strIV, CFB_MODE, Cipher.ENCRYPT_MODE);
    }

    /**
     * 使用AES算法的CFB模式进行解密
     * <p style="color:#ff0">说明：strIV长度不可超过16字节，不要使用汉字</p>
     *
     * @param ciphertext 加密后的 Base64 编码字符串
     * @param hexKey     十六进制的密钥
     * @param strIV      初始化向量（IV）。注：初始向量必须16位字符
     * @return 解密后的明文数据
     */
    public static String decryptCFB(String ciphertext, String hexKey, String strIV) {
        return encryptOrDecrypt(ciphertext, hexKey, strIV, CFB_MODE, Cipher.DECRYPT_MODE);
    }

    /**
     * 使用AES算法的OFB模式进行加密
     * <p style="color:#ff0">说明：strIV长度不可超过16字节，不要使用汉字</p>
     *
     * @param plaintext 明文数据
     * @param hexKey    十六进制的密钥
     * @param strIV     初始化向量（IV）。注：初始向量必须16位字符
     * @return 加密后的 Base64 编码字符串
     */
    public static String encryptOFB(String plaintext, String hexKey, String strIV) {
        return encryptOrDecrypt(plaintext, hexKey, strIV, OFB_MODE, Cipher.ENCRYPT_MODE);
    }

    /**
     * 使用AES算法的OFB模式进行解密
     * <p style="color:#ff0">说明：strIV长度不可超过16字节，不要使用汉字</p>
     *
     * @param ciphertext 加密后的 Base64 编码字符串
     * @param hexKey     十六进制的密钥
     * @param strIV      初始化向量（IV）。注：初始向量必须16位字符
     * @return 解密后的明文数据
     */
    public static String decryptOFB(String ciphertext, String hexKey, String strIV) {
        return encryptOrDecrypt(ciphertext, hexKey, strIV, OFB_MODE, Cipher.DECRYPT_MODE);
    }

    /**
     * 使用AES算法的CTR模式进行加密
     * <p style="color:#ff0">说明：strIV长度不可超过16字节，不要使用汉字</p>
     *
     * @param plaintext 明文数据
     * @param hexKey    十六进制的密钥
     * @param strIV     初始化向量（IV）。注：初始向量必须16位字符
     * @return 加密后的 Base64 编码字符串
     */
    public static String encryptCTR(String plaintext, String hexKey, String strIV) {
        return encryptOrDecrypt(plaintext, hexKey, strIV, CTR_MODE, Cipher.ENCRYPT_MODE);
    }

    /**
     * 使用AES算法的CTR模式进行解密
     * <p style="color:#ff0">说明：strIV长度不可超过16字节，不要使用汉字</p>
     *
     * @param ciphertext 加密后的 Base64 编码字符串
     * @param hexKey     十六进制的密钥
     * @param strIV      初始化向量（IV）。注：初始向量必须16位字符
     * @return 解密后的明文数据
     */
    public static String decryptCTR(String ciphertext, String hexKey, String strIV) {
        return encryptOrDecrypt(ciphertext, hexKey, strIV, CTR_MODE, Cipher.DECRYPT_MODE);
    }

    /**
     * 根方法，AES加密或解密方法，根据规范传入数据则实现加密或者解密。
     * <p style="color:#ff0">说明：strIV长度不可超过16字节，不要使用汉字</p>
     *
     * @param data          明文或者密文数据。注：密文数据需要Base64格式字符串
     * @param hexKey        十六进制的字符串密钥
     * @param strIV         若非ECB模式下需要传入初始向量。注：初始向量必须16位字符（不可使用中文）
     * @param aesMode       指定AES加密的五种模式：算法类型/模式/填充方式（ECB、CBC、CFB、OFB、CTR）
     * @param modeSelection 模式选择：传 1（加密）、传 2（解密）
     * @return 加密或解密的字符串数据信息
     */
    public static String encryptOrDecrypt(String data, String hexKey, String strIV,
                                          String aesMode, Integer modeSelection) {
        // 初始化返回加密或解密的数据
        String result = "";

        // 传入数据处理及校验(若密钥信息为空，初始向量为空则使用默认配置SECRET_KEY和STR_IV)
        modeSelection = modeSelection == null ? 0 : modeSelection;
        if (hexKey == null || hexKey.equals("")) {
            hexKey = SECRET_KEY;
        }
        if (!dataVerification(data, hexKey, aesMode)) {
            throw new RuntimeException("AES加密解密失败：请检查数据、密钥、模式是否为空！");
        }

        // 初始标记变量
        // 是否为特殊模式（CBC、CFB、OFB、CTR），基础模式极为非特殊模式（ECB）
        boolean isSpecialMode = false;
        // 当前是加密模式还是解密模式
        int encryptOrDecryptMode = modeSelection == 1 ? Cipher.ENCRYPT_MODE :
                modeSelection == 2 ? Cipher.DECRYPT_MODE : modeSelection;

        // 校验加密模式或者解密模式是否正确
        if (encryptOrDecryptMode == 0) {
            throw new RuntimeException("AES加密解密失败：请指定正确加密或解密模式，当前模式为：" + modeSelection);
        }

        // 校验AES模式是基础ECB模式还是其它模式；else为ECB模式
        if (specialModes.contains(aesMode)) {
            // 标记特殊模式
            isSpecialMode = true;
            // 如不为ECB模式则需要进行校验初始向量信息，若为空则设置默认的
            if (strIV == null || strIV.equals("")) {
                if (STR_IV == null || STR_IV.equals("")) {
                    throw new RuntimeException("AES加密解密失败：链式反馈模式或计数器模式缺失初始向量！");
                } else {
                    strIV = STR_IV;
                }
            }
        }

        // 正式加密或解密异常处理
        try {
            // 将十六进制的密钥换为字节数组，并创建AES算法的密钥规范对象
            byte[] keyBytes = hexToBytes(hexKey);
            SecretKeySpec secretKey = new SecretKeySpec(keyBytes, ALGORITHM);

            // 创建加密解密对象并指定 算法/模式/填充方式
            Cipher cipher = Cipher.getInstance(aesMode);

            // 特殊模式处理
            if (isSpecialMode) {
                // 初始化AES加密或解密的向量信息
                IvParameterSpec ivParameterSpec = new IvParameterSpec(strIV.getBytes());
                // 初始化为加密器或解密器，并设置密钥规范信息和初始向量
                cipher.init(encryptOrDecryptMode, secretKey, ivParameterSpec);
            } else {
                // 初始化为加密器或解密器，并设置密钥规范信息
                cipher.init(encryptOrDecryptMode, secretKey);
            }

            // 处理加密或者解密
            // 若为1则是：Cipher.ENCRYPT_MODE（加密模式）
            if (encryptOrDecryptMode == 1) {
                // 对明文数据进行加密，得到加密后的字节数组
                byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
                // 使用 Base64 编码将加密后的字节数组转换成字符串，并返回结果
                result = Base64.getEncoder().encodeToString(encryptedBytes);
            }
            // 若为1则是：Cipher.DECRYPT_MODE（解密模式）
            if (encryptOrDecryptMode == 2) {
                // Base64解码加密后的字符串
                byte[] encryptedBytes = Base64.getDecoder().decode(data);
                // 对密文数据进行解密，得到解密后的字节数组
                byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
                // 将解密后的字节数组转换为字符串，并返回结果
                result = new String(decryptedBytes, StandardCharsets.UTF_8);
            }
        } catch (NoSuchAlgorithmException e) {
            log.error("算法不存在异常：{}", e.getMessage());
        } catch (NoSuchPaddingException e) {
            log.error("填充模式不存在异常：{}", e.getMessage());
        } catch (InvalidKeyException e) {
            log.error("无效密钥异常：{}", e.getMessage());
        } catch (InvalidAlgorithmParameterException e) {
            log.error("无效算法参数异常：{}", e.getMessage());
        } catch (IllegalBlockSizeException e) {
            log.error("非法块大小异常：{}", e.getMessage());
        } catch (BadPaddingException e) {
            log.error("错误的填充异常：{}", e.getMessage());
        }

        // 若无法处理返回 result = "" 则抛出异常
        if (result == null || result.equals("")) {
            throw new RuntimeException("AES加密解密失败，请参考之前异常信息！");
        }
        return result;
    }

    /***
     * 字符串转十六进制
     * @param str 字符串信息，如："hello"
     * @return 返回十六进制字符串信息
     */
    public static String strConvertHex(String str) {
        // 判断字符串是否为空
        if (str == null || str.equals("")) {
            return "";
        }
        try {
            // 字符串转为十六进制数据
            byte[] bytes = str.getBytes(CHARSET);
            return bytesToHex(bytes);
        } catch (UnsupportedEncodingException e) {
            log.info("当前编码方式不支持：{}", e.getMessage());
        }
        return "";
    }

    /**
     * <h4>生成十六进制的密钥信息（主要面向AES加密）</h4><br/>
     * <h3>seed!=null：</h3>
     * &nbsp;&nbsp; 根据提供的种子信息生成一个标准的指定长度密钥；(只要种子相同，序列就一样，所生成的秘钥就一样)。<br/>
     * <h3>seed==null：</h3>
     * &nbsp;&nbsp; SecureRandom会使用系统当前的时间、内存信息、操作系统提供的熵（entropy）等来生成随机种子。<br/><br/>
     * <b>说明：我们传入的keySize只有128、192、256这几种，但是返回的却不是这么长的字符是因为，128它是二进制，
     * 而最终返回的是十六进制的数据；4个二进制为1个十六进制，所以128/4=32位十六进制字符长度</b>
     *
     * @param seed    种子字符串信息（内部会转成byte，1个字符8位，一个汉字24位）
     * @param keySize 生成的密钥长度位（可选128、192、256）
     * @return 密钥字符串信息，虽然传入的是128、192、256，但是这个二进制不利于理解，所以转换为十六进制数
     */
    public static String generateAesHexKey(String seed, Integer keySize) {
        // 安全性的随机数生成类（如AES、DES等加密时使用到）
        SecureRandom secureRandom = null;
        // 若传null则使用默认的种子生成
        if (seed == null || seed.equals("")) {
            secureRandom = new SecureRandom();
        } else {
            secureRandom = new SecureRandom(seed.getBytes());
        }
        // 初始化 key生成器
        try {
            KeyGenerator kg = KeyGenerator.getInstance(ALGORITHM);
            // 生成指定长度的密钥长度 单位bit
            kg.init(keySize, secureRandom);
            // 生成一个Key
            SecretKey secretKey = kg.generateKey();
            // 转变为字节数组
            byte[] keyBytes = secretKey.getEncoded();
            // 生成密钥字符串返回(生成的二进制密钥转十六进制返回)
            return bytesToHex(keyBytes);
        } catch (NoSuchAlgorithmException e) {
            log.info("AES密钥生成出现异常：{}", e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }

    /**
     * 校验基本传入的数据是否合法
     *
     * @param dataStr 明文或密文数据
     * @param hexKey  十六进制密钥
     * @param aesMode AES加密解密模式
     * @return 校验合法返回true，校验不合法返回false
     */
    private static boolean dataVerification(String dataStr, String hexKey, String aesMode) {
        // 传入的数据判空及传入的数据转换
        if (dataStr == null || dataStr.equals("")) {
            log.error("使用AES加密出现异常：明文或密文数据不可为空！");
            return false;
        }
        if (hexKey == null || hexKey.equals("")) {
            log.error("使用AES加密出现异常：十六进制密钥数据不可为空！");
            return false;
        }
        if (aesMode == null || aesMode.equals("")) {
            log.error("使用AES加密出现异常：AES加密解密模式不可为空！");
            return false;
        }
        return true;
    }

    /**
     * 二进制数组转为十六进制字符串工具
     *
     * @param bytes 二进制数组
     * @return 十六进制字符串
     */
    public static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        // 遍历二进制数组中的每个字节
        for (byte b : bytes) {
            // 将字节转换为两位十六进制字符串
            String hex = String.format("%02X", b);
            sb.append(hex);
        }
        // 返回最终的十六进制字符串
        return sb.toString();
    }

    /**
     * 十六进制字符串转为二进制字节数组工具
     *
     * @param hex 十六进制字符串
     * @return 十六进制字符串
     */
    public static byte[] hexToBytes(String hex) {
        int length = hex.length();
        byte[] bytes = new byte[length / 2];
        // 每两个字符解析为一个字节
        for (int i = 0; i < length; i += 2) {
            String hexByte = hex.substring(i, i + 2);
            byte b = (byte) Integer.parseInt(hexByte, 16);
            bytes[i / 2] = b;
        }
        // 返回最终的二进制数组
        return bytes;
    }

    /***
     * 示例demo，仅供参考（使用ECB和CBC两种模式测试）
     * @param args ""
     */
    public static void main(String[] args) {
        // 明文数据
        String plaintext = "{'name': '张三', 'age': 25, 'city': '北京', 'hobbies': ['篮球', '游泳', '旅行'], " +
                "'education': {'university': '清华大学', 'major': '计算机科学'}}";
        // 生成密钥
        String aesHexKey = generateAesHexKey("!#$%#^@#$^#$", 128);

        // 调用ECB简单模式加密解密
        String encryptECB = AESTools.encryptECB(plaintext, aesHexKey);
        System.out.println("使用ECB模式加密的数据：" + encryptECB);
        String decryptECB = AESTools.decryptECB(encryptECB, aesHexKey);
        System.out.println("使用ECB模式解密的数据：" + decryptECB);

        // 调用CBC特殊模式加密解密
        // 密钥若为null或”“则使用默认密钥
        String strIV = "yesedtrf45rplesd";  // 初始向量必须为16位；若为null或”“则使用默认的
        String encryptCBC = AESTools.encryptCBC(plaintext, null, strIV);
        System.out.println("使用CBC模式加密的数据：" + encryptCBC);
        String decryptCBC = AESTools.decryptCBC(encryptCBC, null, strIV);
        System.out.println("使用CBC模式解密的数据：" + decryptCBC);
    }
}
